feat: Genie Agent managed-memory agent panel + bootstrap runbook - #7
Open
zuckerberg-db wants to merge 154 commits into
Open
feat: Genie Agent managed-memory agent panel + bootstrap runbook#7zuckerberg-db wants to merge 154 commits into
zuckerberg-db wants to merge 154 commits into
Conversation
…rcel-native SPN proxy - Vendor databricks/app-templates as a sparse submodule (agent-openai-agents-sdk, e2e-chatbot-app-next) with an agent/ overlay + assemble_agent.sh. - Add a slide-out Agent panel (store + trigger) to the sso-spn org view. - Embed the managed-memory Databricks App via a same-origin Vercel route (api/agent-proxy) that mints the current user's SPN token (guest/BYOD) and injects it as a bearer, streaming responses (chat SSE). No Go proxy / Cloud Run needed for the agent. - Chat UI overlay patches: base:"./" + a mount-prefix fetch shim so assets and /api/ calls resolve under the proxy subpath.
- proxy: buffer request body with Content-Length (chunked/duplex POSTs were rejected by the Databricks Apps front door with "Proxy error:") - proxy: inject <base href> + force next-themes light so the embedded chat UI resolves relative assets under the mount and matches Firefly's light UI - proxy: serve the injected HTML no-store and drop conditional request headers so per-request rewrites always reach the browser (app ETag never changes) - panel: point iframe at /api/agent-proxy (no trailing slash) to skip the 308 - build: exclude agent/, agent-build/, vendor/ from the Next TS build and add .vercelignore so the frontend build ignores the agent overlay + submodule
Add a dedicated /docs/solutions/agent page (with architecture sequence diagram) and Solutions-nav entry for the managed-memory agent panel, and document the agent panel + Genie One usage across README, CLAUDE, AGENTS, and .env.example. - New docs-site page src/app/docs/solutions/agent/page.tsx + public/solutions/agent/architecture.mermaid - Proxy implementation detail lives under Backend Configuration, not the overview - README: Agent Panel section, "How the agent uses Genie One", agent env vars - CLAUDE/AGENTS: Vercel-native proxy + submodule/overlay notes - .env.example: NEXT_PUBLIC_AGENT_ENABLED, DATABRICKS_AGENT_APP_URL
The parent repo gitignores agent-build/, and `databricks bundle deploy`
respects the enclosing repo's ignore rules, so sync found zero files
("no files to sync") and would ship an empty app. assemble_agent.sh now
runs a local-only `git init` on agent-build/ to scope the bundle to it.
Verified: `databricks bundle validate` now reports 0 "no files to sync"
warnings.
The Build & deploy section was happy-path only. Add the generic operational detail so anyone cloning the fork can deploy without tribal knowledge: bundle validate + the "0 files to sync" health check, the post-run HTTP 503 poll (container builds uv/npm/vite before serving), the Python 3.12 pin for the PyO3 cap, overlay-applied sanity checks, and why assemble_agent.sh git-inits agent-build.
The agent panel's /api/agent-proxy returns 400/401 unless the user's active org has a workspaceUrl and a userSpns mapping for the user's email. Spell this out in the "Enable it" section so it isn't a silent failure.
- add prerequisite section for provisioning the agent's Databricks resources (MLflow experiment + Lakebase via quickstart, wheels volume) - document the post-deploy Lakebase permission grant required for memory - add the required `bundle run` app resource key and readiness check via `databricks apps get ... RUNNING` - list the Agent-panel SSO->SPN + guest env vars for Vercel deployment - flag still-unverified claims (empty wheels volume sufficiency, lakebase name rules) inline for validation Flagged UNVERIFIED items are inferred, not yet proven by a negative deploy test.
Genie One is queried as the app's service principal, so it only returns UC data that SP can access. Without grants, data questions return empty. Document the required USE CATALOG/USE SCHEMA/SELECT (+ warehouse access), flagged pending verification of the exact minimal grant on a clean workspace.
… ordering Replace hardcodes with bundle variables/derivation and fix the deploy flow based on end-to-end cleanroom proof on a fresh workspace: - Parameterize wheels-volume + memory-store namespace as bundle vars defaulting to workspace.default (works on Default-Storage workspaces with no `main`); derive HOST/WORKSPACE_ID/GENIE_ONE_URL at runtime instead of storing them. - Sync pyproject.toml (not uv.lock) and install build deps offline from pre-vendored cp311 wheels via UV_FIND_LINKS (scripts/vendor_wheels.sh), avoiding the dead .dev proxy, the lagging .cloud mirror, and the 10-min startup wall. - Frontend Drizzle migrations need no manual Lakebase grant: the Postgres resource binding is CAN_CONNECT_AND_CREATE, which auto-provisions the app SP's Postgres role at deploy. Drop the pre-build grant step. - Add scripts/setup_memory_store.py and wire it into scripts/deploy_agent.sh: the durable memory is a UC memory store (not Lakebase) that must be created and granted READ/WRITE to the app SP — otherwise memory silently no-ops. - start_app.py builds the frontend before binding the port so failures are honest in the live app_status; deploy state goes green at container-start.
- GAP-19: okta plugin in auth-dynamic.ts was unconditionally constructed with process.env.SPN_AUTH_OKTA_CLIENT_ID!, crashing Next.js prerendering when the env var is absent. Now guarded: plugin is only registered when all three SPN_AUTH_OKTA_* vars are present. - GAP-20: NEXT_PUBLIC_BETTER_AUTH_URL baked at build time causes CORS on Vercel preview deployments. Updated .env.example with an explicit warning (commented out by default); updated README to say do not set this var unless using a stable custom domain on all deployments. - GAP-21: Guest login was documented as "Coming Soon." Added a Guest User Provisioning section to README with the correct 3-step API sequence (workspaces → spns → users), required fields, and accurate response shapes. - GAP-22: Added a Vercel deployment step documenting how to disable SSO protection on preview deployments. Corrected CLI command to the real `vercel project protection disable <name> --sso` (v54+ syntax).
BOOTSTRAP.md covers all 22 documented gaps with corrected commands, a full interactive-auth flow (browser OAuth for Databricks, Neon, Vercel, GitHub — no manual key entry), and a proven vs. inferred gap reference table. scripts/bootstrap.sh implements the runbook as an executable shell script with --dry-run and --stop-after=N flags for iterative testing.
Fixes missing step for creating a Databricks service principal with M2M OAuth credentials for the guest login flow. Phase 9 now reads client ID and secret from keyring instead of using a placeholder. Also adds DATABRICKS_ACCOUNT_ID to Phase 0 inputs.
…9 keyring reads - Phase 8b split into Tier 1 (guest login, required) and Tier 2 (admin Databricks OAuth, placeholder-safe for guest-only verification) - Documents why U2M vars don't crash the build (plain object vs okta() call) - Phase 8d stores PREVIEW_URL and GUEST_API_SECRET in keyring - Phase 9 loads all vars from keyring — no implicit shell carry-over
- NG-1: workspace list requires PATH arg (workspace list / --profile)
- NG-2: delete stale HOST/WORKSPACE_ID/GENIE_ONE_URL yml-edit instructions;
replace with note that these are runtime-injected; add catalog/schema
override guidance only when non-default
- NG-3: AGENT_APP_NAME default → firefly-openai-managed-mem-v2 (bundle hardcodes this)
- NG-4: remove stale re-assemble step from Phase 4 (wipes quickstart .env + wheels)
- NG-5/6: run bundle deploy/run from agent-build/; add resource key
(agent_openai_agents_sdk) to bundle run command
- NG-7: pass --memory-store to setup_memory_store.py
- NG-8: UC permissions → PATCH /api/2.1/unity-catalog/permissions/catalog/{name}
(POST is not a valid method)
- NG-9: warehouses list -o json returns bare array (not {"warehouses":[]}); use
/api/2.0/permissions/warehouses (not preview/sql endpoint, which returns 405)
- NG-10: SP create output uses SCIM camelCase applicationId (not application_id)
- NG-11/14: replace account-API/curl/UI-fallback block with
service-principal-secrets-proxy create (workspace-level, no account admin needed)
- NG-12: DB_URL already stored in keyring; ensure drizzle step reads it
- NG-13: add pnpm install before drizzle-kit push (node_modules absent by default)
Also fix unclosed code block fences in Phases 3a, 3d, 4, 5.
GitHub strips cursor:// links from README markdown, so the badge opened the shields.io image instead of launching Cursor. Route through cursor.com/redirect so the deeplink handoff works from github.com.
…adge cursor.com/redirect does not exist (404). GitHub also strips cursor:// links, so use Cursor's documented HTTPS prompt handoff to open the genie-agent bootstrap workflow.
Add an explicit STOP gate so agents must confirm every required input before Phase 1 (including read-only probes). Move [ASK] items to a checklist with [ASK — REQUIRED, BLOCKING] markers. Default clone directory is now the current working directory instead of ~/Projects/firefly.
- Phase 6c: check UC tables only; defer seeding to end-of-runbook guidance - Phase 6b: grant guest SP CAN_USE on warehouse and agent app - End sections: next steps when no UC data; agent drafts and files GitHub issues
Centralize solution metadata, add /docs/solutions hub, and link Agent Panel from architecture pages, cross-links, nav, homepage, and README so it is no longer missing from the lakehouse-apps-proxy documentation grid.
neonctl projects create --output json returns {"project":{"id":...}}, but
Phase 7 read json['id'] and got an empty/wrong value, breaking downstream
DATABASE_URL setup. Read project.id with a flat id fallback for older
neonctl versions.
Fixes #16
Phase 8d captured the preview URL with `grep -E 'https://' | tail -1`, which matched Vercel's trailing "To change the domain… https://vercel.com/…" help line instead of the deployment URL. Phase 9 guest API calls then hit a malformed PREVIEW_URL. Anchor the match to the .vercel.app host. Fixes #15
…al edit
Phase 3b only warned the user to hand-edit agent-build/databricks.yml when
UC_CATALOG/UC_SCHEMA differed from the workspace/default bundle defaults;
skipping the edit broke the deploy or misrouted Genie context. The bundle
already exposes catalog/schema as variables (agent/databricks.yml header),
and DATABRICKS_MEMORY_STORE is the template
"${var.catalog}.${var.schema}.${var.memory_store_name}". Thread the Phase 0
inputs through `bundle deploy/run --var` so no file mutation is needed and
the memory store resolves automatically.
Fixes #12
Phase 1c called `vercel login` without checking the CLI was present, so a fresh machine failed at the OAuth step. Add an install check (npm i -g vercel) mirroring the neonctl/pnpm pattern, so no CLI is assumed pre-installed. Fixes #8
vercel link (Phase 8) needs a repo the deployer can write to, but the clone points at databrickslabs/firefly (no external write access). After cloning, create or reuse a user-owned fork and push genie-agent to it, so Vercel's Git integration links to that remote. Honors SKIP_GITHUB_PUSH and GITHUB_FORK. Fixes #17
On machines behind a corporate MITM proxy (Zscaler etc.), quickstart.py and the Databricks SDK fail TLS verification and hang until timeout. Phase 0 now accepts a combined PEM (via prompt, or preset TLS_PEM_PATH for CI) and exports REQUESTS_CA_BUNDLE / SSL_CERT_FILE / NODE_EXTRA_CA_CERTS for the session. Fixes #13
… via npm (#9) Reorders Phase 1 so pnpm is installed before the CLIs and frontend build that depend on it, then Vercel, GitHub, Neon, Databricks. - pnpm moved to 1a (was 1d) - GitHub CLI: auto-install from the official release archive into $HOME/bin when absent (no Homebrew dependency), then auth. Deviates from the issue's `brew install gh` suggestion because the target audience is not guaranteed to have Homebrew. - Neon CLI: `npm install -g neonctl` instead of `brew install neonctl` for the same reason. - Install steps are DRY_RUN-guarded so --dry-run never hits the network.
…rting state Three symptoms on one live run, all from Phase 8/9 trusting variables that only exist in the first shell: init_state_dir fell back to $PWD, so a phase invoked from anywhere but the repo read a DIFFERENT state.env. Phase 9 concluded GUEST_API_SECRET was "0 chars" and spent about three minutes reminting and redeploying a secret that was already stored correctly. The location is now recovered from inputs.env, making it a property of the bootstrap rather than of the caller's directory. Phase 8d does store_secret PREVIEW_URL "$APP_ORIGIN". In a fresh shell APP_ORIGIN was unset, so a good value was overwritten with "" and Phase 9's guest-login curls returned HTTP 000 with nothing to explain it. store_secret now refuses to overwrite a stored value with an empty one and says the variable was unset, and Phase 8d recovers APP_ORIGIN before using it. Phase 8e printed "Production does not serve " with no hostname, which reads as a domain mismatch when the deployment was correct and aliased. It now names the empty variable and says explicitly that this is not a domain mismatch. Invariant 38 covers both root causes. Its first version failed against correct code because it matched "$PWD" inside this function's own explanatory comment -- the second time a check here has been decided by prose instead of by what executes, so it strips comments before looking.
Phase 8d ends with store_secret GUEST_API_SECRET "$GUEST_API_SECRET", re-storing a value minted back in Phase 8b. After a shell boundary that variable is empty, so the line passes nothing. Nothing was lost -- the empty-write guard added in the previous commit refused the overwrite and kept the stored value, which is how the run reported this as an inconsistency rather than a failure -- but the call was a save that saved nothing, while APP_ORIGIN two lines above already recovered itself first. Three keys had that asymmetry: GUEST_SP_SECRET, GENIE_SPACE_ID and GUEST_API_SECRET. A store immediately after a mint is fine and stays as it was; only a re-store needs the recovery, and invariant 39 encodes exactly that distinction so the next one cannot slip in unnoticed.
…ly script
The deployment summary is the one table an operator forwards to whoever will use the
app, and its second row read
| Expired or already used? | `bash scripts/new-guest-link.sh --open` |
That reader cannot run it. The script needs a clone of this repo and reads PREVIEW_URL,
GUEST_API_SECRET, GUEST_SP_CLIENT_ID and GUEST_SP_SECRET from
.firefly-bootstrap/state.env, exiting and naming whichever is missing. So the row was
useless to the person reading it, and the only way to make it work would be to hand over
guest-SP credentials.
The app already handles this correctly -- the guest-login page says "ask whoever shared
it to send you a fresh link" -- so the summary was contradicting the product's own UX.
The row now says the same thing, and the command moved to a block addressed to the
operator that states its prerequisites and that those values are credentials to keep
local.
Invariant 40 fails if any row of that table asks its reader for a shell command or names
a secret.
…" where that is the point The app's attribution component has returned 'Genie Agent' for a while; the docs lagged it in 49 places across the runbook, README, docs pages, docstrings and runtime messages. Three of those were not renames. Where "Genie One" was carrying the meaning "workspace-wide", swapping the name alone would have left `one` and `space` reading identically -- both modes ARE the Genie Agent, and the only distinction that matters is workspace-wide versus a single curated space. Those say workspace-wide now, including the fallback messages, where "staying on Genie Agent" would have described falling back to the product itself. Three references keep the old label deliberately, quoted: the passages explaining why a link LABELLED "Genie One" was removed. Renaming a quoted historical label makes its own explanation self-contradictory. Not touched: the mode VALUE `one`. It is an identifier, and renaming it would break bundle variables and GENIE_MCP_MODE for no reader's benefit. Also unchanged is the Genie space title, which already read "Firefly Genie Agent" -- the harness matches our space by exact title, so a rename there would have orphaned every existing space and created duplicates on the next run. Invariant 41 allows the quoted historical label and fails on any other use. Also in this commit: Phase 6's context restore moved above the phase's FIRST consumer. It sat ahead of the warehouse PATCH, leaving the catalog PATCH -- the first command in the phase -- reading an empty $SP_CLIENT_ID, which the API reported as "UpdatePermissions Missing required field: principal", i.e. apparently a request-schema bug. Invariant 36 now derives the consumers from the variables themselves rather than from a list of two I picked, and joins line continuations, because $SP_CLIENT_ID appears on the `--json` continuation of that call and a per-line filter skipped exactly the command that failed.
…k lives on
BOOTSTRAP.md and scripts/ exist only on genie-agent -- origin/main has README.md and
neither of them. That makes every branch reference a time bomb pointing in one of two
directions:
* `git clone --branch genie-agent` is right today and fails the day the branch merges
and is deleted, with "Remote branch genie-agent not found in upstream origin" -- at
Phase 2, the first step a new reader takes.
* Dropping the branch fails right now, because the default branch has no BOOTSTRAP.md.
Neither fixed choice survives the transition, so both clone paths resolve it: prefer the
bootstrap branch while `git ls-remote` still finds it, fall back to the default branch
once it has absorbed the runbook. Phase 2 then asserts BOOTSTRAP.md is actually in the
checkout, because otherwise the first symptom of cloning the wrong branch is a missing
file several phases later, which reads as a broken repo.
The README's "Open in Cursor" deeplink had the same pin and cannot run shell to resolve
it, so its prompt now tells the agent to check for genie-agent and use the default branch
if it is gone.
The issue templates hardcoded "Bootstrap branch: genie-agent" in the Environment block.
After the merge that sends a maintainer to code the reporter never ran, so they report
`git rev-parse --abbrev-ref HEAD` instead.
Invariant 42 fails on all four ways this regresses. Its first version had a dead arm:
piping a repo-wide grep into `grep -q` and setting the flag with `&&` can never fire,
because grep -q exits on first match, SIGPIPEs the upstream grep, and `set -o pipefail`
turns the pipeline non-zero. It passed against a deliberately re-pinned clone until the
pipeline was replaced with a captured variable.
…ing a branch Two changes, both aimed at the same rot. The README's "Open in Cursor" badge encoded the branch inside a cursor.com query string, which no amount of markdown can make branch-relative. It now points at ./BOOTSTRAP.md -- a relative link, which GitHub resolves against whatever ref the reader is viewing. Correct on genie-agent today, correct on the default branch the moment the merge lands, with no branch named in README.md at all. The badge label changed with the target, since a badge reading "Open in Cursor" while pointing at a markdown file is its own small lie. The one-click-into-Cursor convenience is the price; permanent correctness is what it buys. github-source-link.tsx derived its blob URL from a GITHUB_REPO constant and then built its RAW url from a second, separately hardcoded copy of the same owner and repo. Changing the constant would have moved one link and left the other pointing at the old repository -- invisible until a user clicked. Both now come from src/lib/repo-links.ts, and the produced URLs are byte-identical to before; this is a refactor, not a behaviour change. Deliberately NOT unified: which repo and branch each component targets. The docs "edit this page" links and the source-view links are not one fact, and collapsing them into a single constant would be a behaviour change wearing a refactor's clothes. Still UNVERIFIED and left exactly as it was: github-source-link points at stikkireddy/firefly-analytics-monorepo, not databrickslabs/firefly. That component renders in top-nav, sso-spn-top-nav and the sso-spn-admin sidebar, so every page with that nav sends users there to view source. Marked with a comment; deciding it is a product call. Invariant 43 bans hand-built GitHub URL strings outside repo-links.ts. Invariant 42 also had to stop trusting grep's --include/--exclude-dir: on the BSD grep macOS ships, --exclude-dir=.git did not exclude and --include='*.md' matched .git/COMMIT_EDITMSG, a file with no extension -- so the check failed on the very commit message describing the fix. It filters paths explicitly now.
… pin Dropping the deeplink outright traded a real convenience for correctness when both were available. The branch only had to stop being load-bearing. The prompt now says to clone the repo and work through BOOTSTRAP.md, and only then: "if BOOTSTRAP.md is not present on the default branch, check out the genie-agent branch, which has it." Today the default branch lacks the runbook, so the fallback fires. After the merge the condition is simply false and the branch name goes inert -- rather than becoming a clone that fails. That is the difference between naming a branch and depending on one. Both badges now sit side by side: the Cursor deeplink for one-click setup, and the relative ./BOOTSTRAP.md link, which needs no branch at all because GitHub resolves it against whatever ref the reader is on. Invariant 42 still fails on a deeplink that names the branch with no fallback.
…tags error The comment marked this UNVERIFIED on the grounds that the failure only reproduces on a proxied network. A proxied run has now tested it: every vercel command still printed "Error: Failed to fetch dist-tags from npm" first and then succeeded. Keeping the export is still worthwhile for the outbound chatter it does cut, but the runbook should state the tested result rather than leave a reader expecting silence they will not get -- the whole reason that line matters is that it reads like an auth or install failure.
Branch selection was the one thing this runbook decided behind the reader's back. Phase 0 blocks on fifteen [ASK - REQUIRED, BLOCKING] items, so detection-with-fallback was the odd mechanism out, and "preserve the one-click experience" was defending something that does not exist -- the badge opens straight into a wall of Phase 0 questions regardless. The prompt now asks for FIREFLY_BRANCH with a default of main, which is the value that is correct once this branch merges. What makes that default safe before then is the second half: the agent must confirm the branch it cloned actually contains BOOTSTRAP.md and ask again if it does not. Accept the default today and you are told immediately, rather than meeting a missing BOOTSTRAP.md at Phase 2 and reading it as a broken repository. README.md no longer names a branch anywhere. Phase 2's firefly_resolve_branch and the runbook's post-clone assertion stay as they are -- a reader who never touches the badge still needs Phase 2 to work on its own.
…r the checks
github-source-link.tsx pointed at stikkireddy/firefly-analytics-monorepo. That repository
returns 404 from the GitHub API, from github.com and from raw.githubusercontent.com, and
databrickslabs/firefly is not a fork of it (fork=false, no parent) so it was not a rename
either -- the reference was simply dead. The component renders in top-nav, sso-spn-top-nav
and the sso-spn-admin sidebar, so every page carrying that nav offered users a "view
source" link that 404s. Repointed at the repository this code actually lives in; blob and
raw URLs confirmed 200 on both main and genie-agent.
Also adds `make check`, which runs the runbook invariants and the typecheck together.
Actions are disabled on this repository ({"enabled": false} on /actions/permissions), so 55
invariants and a 60-probe regression matrix are enforced by nothing automatic -- they hold
only when somebody runs them. Repo-local git hooks are not an option either: core.hooksPath
points globally at ~/.databricks/githooks, which is not ours to edit. So the least-bad
remedy is to make the gate trivial to invoke and say plainly in the Makefile why it matters.
Enabling Actions remains the real fix and needs a repo admin.
…s the space genie_tools.py held its own `GENIE_MCP_PATH = "/api/2.0/mcp/genie"` and POSTed straight to it, while agent.py resolved the mode properly and pointed the MCP server at /api/2.0/mcp/genie/<space_id>. Both were attached to the same Agent, and GENIE_INSTRUCTIONS tells the model it MUST call the local tool first -- so a deployment configured for a curated space answered from workspace-wide Genie anyway, with the space-scoped server connected and unused. That is not a cosmetic split. Choosing a space is how answers get scoped to curated tables and joins, and the space is the only object a guest can hold CAN_RUN on. Guests have no workspace access, so answering them from workspace-wide Genie cannot work at all -- and every configuration probe still reported mode=space, because the deploy really did pass that variable. Nothing measured which endpoint a question reached, which is why twelve clean trial runs are consistent with this bug. genie_mcp.py now resolves it once; neither file holds a path constant. The default is `space`, since the Genie Agent IS the space, and `one` becomes an explicit opt-out rather than a fallback. Resolution RAISES when the mode wants a space and no id is set instead of quietly degrading -- a silent fallback is what kept this invisible -- and logs the resolved path once so the backend is observable rather than inferred. `space` stays the canonical value on purpose: renaming it to `agent` at the same time as the phase reordering would be two changes at once, each able to mask the other. `agent` is accepted as an alias so that rename is later and safe. `ask_genie_one` is now `ask_genie`, including in the docs page: the old name described a backend it no longer exclusively used. Reordering the phases so the space exists BEFORE the first deploy -- which is what makes this default reachable without a redeploy -- is the next commit.
…sing reader
firefly_store_input chained its dedupe off grep's exit status:
[ -f "$file" ] && grep -v "^${key}=" "$file" > "$file.tmp" && mv "$file.tmp" "$file"
`grep -v` exits 1 when it filters out EVERY line, which is precisely the case where the
file holds only this key. The chained `mv` then never ran, the original survived, and the
append added a second copy -- so re-storing an answer duplicated it and inputs.env grew on
every re-run. Readers that take the last match hid it; one taking the first would have
returned a stale answer.
Also adds firefly_read_input, which was simply missing: the writer had no counterpart, so
init_state_dir inlined its own sed to recover REPO_DIR. A phase that persists an answer and
a later phase that needs it back should not each invent the parsing -- and the reordering
needs to read GENIE_MCP_MODE back in Phase 4.
…ot after The app was deployed workspace-wide because no space existed yet, then Phase 6c created one and redeployed to correct it. Any failure between those two points left the app answering from workspace-wide Genie -- which guest users cannot query at all, having no workspace access -- and nothing said so, because the deploy really had passed the variable it was given. `one` was the default largely because it was the only thing reachable at deploy time. Nothing in space creation needed the app. Creating a space needs a warehouse and tables; only GRANTING a service principal CAN_RUN on it needs the app to exist. So the two halves split cleanly: Phase 3f (new) resolve the warehouse, seed, create/resolve the space Phase 4 deploy ONCE, already carrying genie_mcp_mode + genie_space_id Phase 6c grants only -- no redeploy, because there is nothing to correct The warehouse moves to 3f as well, since seeding and space creation both need it and both now precede the deploy; Phase 6 recovers it from inputs.env rather than resolving it twice. Invariant 32 asserted that Phase 6c's redeploy waited for Phase 4's deployment to settle. That race no longer exists, and worse, the check had begun passing VACUOUSLY: it searched for the literal `genie_mcp_mode=space`, which is now a variable. It is replaced by one asserting the first deploy carries both Genie vars and that no bundle deploy appears at or after Phase 6. Two other checks needed correcting because they described the old shape rather than the requirement. Invariant 36 accepted only `= "space"` with an else, so the natural `!= "space"` form read as a silent gate; it now anchors on each gate and requires every one of them to speak, having first passed while one gate was deliberately silenced behind a talkative one. Invariant 39 could not see that `eval "$(...)"` mints a variable, so Phase 3f's mint-then-store looked like an unguarded re-store.
…refused Phase 3f creates the Genie space before any service principal exists, so it cannot grant and deliberately passed no --grant-guest. A missing flag defaulted to no, so a run whose operator had answered yes was told the guest SP would get NO access to the space -- and Phase 6c then granted it. The flag really was absent; what the reader takes from it is that their Phase 0 answer was discarded. A trial reported it. Deferral is now its own state: 3f passes --defer-grants and the script names where the grants land instead of warning about access nobody withheld. The refusal warning still fires for a real no. Invariant 44 covers it, and found two things I had not looked at while I was writing it: - the documented seed-recovery command creates a space and passes no --grant-guest at all, after the SPs exist -- so it silently dropped guest access for anyone following it. Fixed to carry the answer. - my first version of the guard ranged /Phase 3f/,/Phase 4/, and 'Phase 4' appears in prose 350 lines earlier, so the window was three lines long and it failed a file that did carry the flag. Re-anchored on the invocation. The rule is now that a creating call must be explicit either way; silence is what is banned. Both halves verified by sabotage. The first sabotage attempt was a silent no-op and 'passed' for that reason, which is the same failure mode as the guard above.
Two more instances of the defect fixed in c4be845, both reported by the second pass of the same trial: - The grants-only call passed --seed no, so a run whose operator answered yes -- and where Phase 3f had already seeded 16 tables -- was told seeding was 'declined at Phase 0'. --seed now takes skip, meaning not this call's concern, and only a real no is reported as one. - Every message hardcoded 'Phase 6c', so a reader in Phase 3f got 'Phase 6c using warehouse ...' and went looking for a phase they had not reached. The caller passes --phase. SEED_STATUS=not-this-phase is documented next to the values it could be confused with. Both fixes verified by extracting the real branch and running it under bash and zsh. The two new invariant clauses were first written as a grep over the whole file and both were vacuous -- the dry-run banner mentions --seed skip, and PHASE_LABEL appears in its own comment -- so sabotaging either changed nothing and the guard still passed. They are now scoped to the invocation and to the message, and each has been sabotaged and seen to fail.
firefly_restore_phase6_context printed 'GENIE_SPACE_ID is set but GENIE_MCP_MODE was not - assuming space' on a run where Phase 3f had already written the mode down. store_secret writes state.env and firefly_store_input writes inputs.env; the restore read only state.env, so the mode was never found. The message was accurate about what the function could see and wrong about what was known, which is the shape of defect this runbook keeps producing. It now reads both stores, and when the value really is in neither it says so by naming them instead of announcing an assumption. Verified under bash and zsh for the found case, the genuinely-absent case, and that both values reach the caller when not piped. Invariant 45 covers it. Its first version matched the comment that explains the old message and failed the fixed code, and the block had been inserted twice so it reported the same thing in both directions at once. Comments are stripped, the range is bounded to the first definition, and the duplicate is gone.
npm install -g vercel prints nothing for about 40 seconds. Phase 3a carries a warning for exactly this shape of problem -- a harness that backgrounds a command after ~30s returns its own exit 0 while the command is still working, so a slow silent step is indistinguishable from a hang -- and Phase 1d had no equivalent. A run reported it.
…able table grant_table_select sent every GRANT to /dev/null and counted the ones that returned 0, so a run that seeded 16 tables printed 'granted SELECT on 15 table(s)' and left the reader to notice the difference and explain it. A table the guest cannot read looked exactly like one it can. Failures are now counted and named with the error that caused them, and the space's coverage is compared against the schema so a seeded-but-uncovered table is stated outright -- Genie cannot query such a table, which is a real question worth asking and impossible to ask if nobody says which table is absent. I am not claiming to know why the 16th table was outside the space on that run; there is no evidence here for that, and this makes the next run say so instead of printing a number. Both paths verified under bash and zsh with stubs that fail the way a permission error does. Invariant 46 covers it, verified by restoring the silent version.
…will beat The note added in ae7ac7f said the install is silent for ~40s. The next run took ~2 minutes and reported it as a gap: a 180s wait had backgrounded the install mid-command, and the reassurance had become the misleading thing. Quoting a duration invites planning around it. It now says the duration is unpredictable, gives both observed figures as evidence that no single number is safe, and gives the actionable rule instead: wait on the process rather than a clock, and do not run a second global install alongside it.
…ng host After vercel deploy --prod the CLI prints a per-deployment host labelled Production and offers 'Promote to production' as the next step. Both read as 'not live yet at the origin you want', when the serving origin was already correct and Phase 8a-2 had read it from the domains API. A run reported being sent toward a second unnecessary deploy. The text belongs to Vercel, so the fix is to say so where the command is run rather than to pretend the output will change.
Both the README and the user-facing docs page still described the behaviour we inverted: that GENIE_MCP_MODE defaults to 'one' and the agent answers from workspace-wide Genie. It defaults to 'space' and answers from a curated Genie space. Anyone following either document would have configured the opposite of what ships. The docs page was worse than the README. It called Genie Agent 'the workspace-wide unified Genie' -- the exact conflation this branch exists to remove -- and stated the agent 'is not scoped to a single Genie space'. It also advertised a 'Powered by Genie / Genie Agent' link that was deleted along with the attribution. Only 'space' is surfaced now. Workspace-wide remains supported in code but is no longer offered as a choice, since it cannot serve guests: a space is the only object a guest SP can be granted CAN_RUN on. GENIE_MCP_URL is undocumented for the same reason -- it only takes effect in the mode we no longer surface. Also corrected, all verified against source rather than assumed: - ask_genie_one -> ask_genie (renamed with the shared resolver) - genie_space_id default is 'none', not empty; empty makes the Apps API reject the deploy - the space is created in Phase 3f before the deploy, not in Phase 6c afterwards - the 'hardcodes the MCP path / feeds the attribution-link fallback' note was wrong in both halves; the path is resolved centrally and the link is gone - 'this repo ships no dataset' -- Phase 3f seeds samples.wanderbricks by default - 'all nine phases' -> Phases 0-9 with sub-phases 3f, 6b, 6c - the UNVERIFIED grants comment narrowed to what runs actually proved: the listed privileges are sufficient; the minimal set is still unknown because nobody has removed them one at a time - unpinned 'pnpm install -g vercel' -> pinned npm, matching Phase 1d - duplicate '### 6.' headings renumbered Typecheck clean, all 46 invariants hold, and the page was rendered locally to confirm.
A real bootstrap run against a real workspace found what nine consecutive green end-to-end runs did not: space mode was broken, and space is the default. /api/2.0/mcp/genie genie_ask, genie_poll_response /api/2.0/mcp/genie/<space_id> query_space_<id>, poll_response_<id> 87ba962 moved the PATH into one resolver and left the tool names hardcoded to the workspace-wide pair one layer down, so the deployed agent POSTed genie_ask to a space endpoint and got '-32602 BAD_REQUEST: Tool genie_ask does not exist'. Fixing the path while leaving the names was the same defect with a smaller blast radius. The argument names, id casing, status vocabulary and answer location differ too: space mode takes and , returns camelCase ids and uppercase states, and puts the answer in rather than . genie_mcp.genie_tool_names() resolves the pair and genie_tools normalises both shapes to one. Second defect, same run: a JSON-RPC error returned {} and discarded the message, so a precise server explanation reached the user as 'Genie ask failed: {}'. That is the discarded-stderr antipattern invariant 27 forbids in the runbook, in the agent this time. Errors now propagate. Third: new-guest-link.sh could not find DATABRICKS_HOST -- the host is a Phase 0 answer in inputs.env, not a secret in state.env -- and defaulting the workspace URL to the app origin made the app POST client_credentials to <app-origin>/oidc/v1/token and parse its own 404 page, which surfaced as 'Failed to obtain Databricks OAuth token'. Fixes authored during that run; I reviewed each against the source and adopted them. Tests, because the gap was that nothing exercised this layer: - agent/tests/test_genie_backends.py, 14 cases over tool names, argument names, error propagation and both response shapes. Restoring the hardcoded names fails 5; restoring fails 2. The OpenAI Agents SDK is stubbed so they run on a clean checkout. - invariant 47: names resolved per backend, and the JSON-RPC error reaches the caller. Its error-propagation clause first grepped the whole file, was satisfied by an unrelated , and passed the original bug; it is now scoped to the branch. - make check runs the unit tests alongside the invariants.
…g the resource
Three runs against one workspace took the Genie space count from 2 to 3. The lookup that decides
reuse-or-create had three compounding defects:
1. It listed spaces with stderr sent to /dev/null, through a parser returning {} on any failure,
so "the API did not answer" and "no such space exists" were the same value -- and the branch
on that value CREATES. One hiccup produced a second space, with the evidence destroyed by the
same line that caused it. Reuse-or-create has three outcomes, not two.
2. It took the first title match and broke, so once two spaces shared a title, which one got
reused was whatever order the API returned, and duplicates were undetectable. That is why a
later run reused an older space rather than the one created 30 minutes earlier.
3. It read one page and never followed next_page_token, so correctness depended on a space count
nobody was watching.
Now: the call is checked and an unknown result REFUSES to create while naming the error, every
page is read, all matches are counted, and duplicates are reported with a deterministic choice
(lowest id) so repeated runs agree.
Verified with a stubbed API across four paths: single match reuses; two matches warn and pick
deterministically; no match still creates; a failed list refuses and says why.
Invariant 48 covers it, verified by restoring the original lookup verbatim -- four of its five
clauses fire.
What I have NOT established is which of the three defects produced the specific duplicate I was
asked about. ours_count stayed 1 across all three runs while the total rose, and with the same
catalog.schema on both runs the titles should have matched, so either the title differed or the
count was wrong. Nothing recorded enough to say which, because the line that would have shown it
is the one that discarded the error. The harness now records every space title, so the next
occurrence is answerable rather than arguable.
This is the answer to why more than one Genie space gets created. The lookup printed "Genie spaces visible: 0" on a workspace holding four spaces, and created another. An empty list that returns exit 0 is indistinguishable from a genuinely empty workspace, so the exit-status check added in daf2bbb does not catch it -- and the fallback on "no spaces exist" is to create. Bootstrap already knows better: it wrote the space id down. If the list cannot see a space we recorded, and that space is still readable, the LIST is wrong and the record is right. It now reuses the recorded space and says the list was inconsistent, rather than creating a second one. When the recorded space is genuinely gone, creating a replacement is correct and it says that too. Verified with stubs across four paths: empty list with a readable recorded space reuses it; empty list with a deleted one creates; empty list with nothing recorded creates; the "none" sentinel is not treated as an id. Two guesses I made along the way were wrong, which is worth recording because both sounded plausible and neither survived contact with the evidence. First, that the runs were creating duplicates of the same title -- ours_count stayed at 1 throughout, so they were not exact duplicates. Second, that a transient list FAILURE caused it -- the list did not fail, it returned success with nothing in it. The full titles and the visible-count that the harness now records are what settled it.
firefly_install_databricks_cli resolved "latest" through an unauthenticated call to api.github.com, which allows 60 requests per hour per IP. A machine running the runbook repeatedly exhausts that, and the whole run then dies: Phases 3a-6 and 8b-9 all fail without the CLI, with no recovery path. That is what happened on the run meant to validate the Genie space fix -- it never reached Phase 3f, so the validation learned nothing. The message made it worse. GitHub answers 403 with a body saying "API rate limit exceeded", and the installer reported "could not resolve the latest Databricks CLI release", which sends the reader to networking or to the Databricks release page. Neither is the problem. Three changes: - Authenticate when a token exists: GH_TOKEN, GITHUB_TOKEN, or `gh auth token`. That raises the limit from 60/hour to 5000/hour, and gh is already installed by Phase 1e. - When the API still cannot answer, install FIREFLY_DATABRICKS_CLI_FALLBACK (1.9.0) rather than failing. The download host is not API-rate-limited, so the fallback works precisely when the lookup does not. It is a floor, not a preference -- every phase asserts the CLI works, not its version. - Say what a 403 here usually means, and only suggest a token when none was supplied. Verified across three paths with a stubbed curl: a normal answer resolves the real tag; a 403 without a token falls back and names the rate limit; a 403 with a token falls back without offering token advice that would not help.
…never matched our own spaces
This is the actual cause of the accumulating Genie spaces, and it is not what my two previous
commits said.
The workspace holds these titles:
Firefly Genie Agent — <cat>.default 2026-07-29 18:15:23
Firefly Genie Agent — <cat>.default 2026-07-29 17:30:18
Firefly Genie Agent — <cat>.default 2026-07-29 17:02:24
Firefly Genie Agent — <cat>.default 2026-07-29 15:44:23
Firefly Genie Agent — <cat>.default
space_title_for adds no timestamp -- it is `printf 'Firefly Genie Agent — %s.%s'`. The server
appends one on creation. So the reuse lookup, which tested title EQUALITY, could never match a
space this script had created: only the one bare-titled space matched, every other run concluded
no space existed, and each created another. That is also why ours_count sat at 1 across every run
while the total climbed to six.
The matcher now accepts our title followed by a timestamp, and only a timestamp -- a bare prefix
test would also swallow a different schema whose name extends ours (default vs default_other),
which is checked.
Two earlier explanations were wrong and are worth naming, because each was plausible and each sent
me somewhere useless:
- daf2bbb: a transient list FAILURE. The list did not fail.
- ffee3de: an empty-but-successful list. Real, and the cross-check it added is worth keeping, but
it was not the cause -- and it could not have been the whole story, because bootstrap runs on a
fresh VM where state.env holds no prior space id to cross-check against.
Still unexplained: the product's own list printed "Genie spaces visible: 0" on the same run where
the harness listed six spaces on that workspace. Same profile, minutes apart. The prefix fix makes
reuse work whenever the list is populated; it does nothing when the list comes back empty, and I do
not yet know why it does.
"Genie spaces visible: 0" on a workspace holding several spaces was my own bug, introduced in daf2bbb when I added pagination. That version appends each page to a file and parses it LINE BY LINE, but `databricks api get` pretty-prints, so every line is a fragment, every json.loads fails, and every page counts zero. The version before it piped the whole response into json.load, which does not care about newlines. Each page is now compacted to a single line before it is appended. Verified side by side against a pretty-printed response: the fixed reader sees both spaces and reuses the lower id; the line-by-line parse sees zero. This invalidates two explanations I committed with confidence: - ffee3de blamed an empty-but-successful list and added a cross-check against the recorded space id. The list was never empty. The cross-check is still correct and worth keeping -- it just never fired, because on a fresh VM state.env holds no prior id. - 1092c5e blamed the server-appended title timestamp. That one is real and confirmed from the recorded titles, and equality genuinely could not match our own spaces. But it was not what the runs were hitting after daf2bbb, because the reader was returning zero spaces before the matcher ever ran. Both fixes stay. The order they were found in is the reverse of the order they mattered, and the reason I chased two wrong causes first is that the diagnostic I added to explain the behaviour -- the visible-count -- was itself reporting the bug I had just written.
… disabled
Raised by a reviewer, and confirmed: the MCP tool-denial short-circuit in chat.ts sat inside
`if (dbAvailable && requestBody.previousMessages)`, so with no database a continuation carrying a
denial skipped the check and fell through to streamText. The model was called against a tool the
user had just refused.
Ephemeral mode is not a corner: the handler documents it ("Works in ephemeral mode when database is
disabled") and logs it on entry, so this is a supported configuration in which a user denial did
nothing.
The check was nested one level deeper than the report described -- also inside
`assistantMessages.length > 0` -- while hasMcpDenial scans ALL previous messages, not just
assistant ones. So a denial was also skipped when a continuation carried no assistant messages at
all, regardless of the database.
Both guards now cover only saveMessages, which is the sole part that needs a database. Persistence
and permission are unrelated concerns.
Verified: the patched file produces the same ten TS2307 module-resolution errors as HEAD and no
syntax or type faults (the vendored app is not checked out locally, so those are expected and
unchanged). Invariant 49 asserts the denial check is not inside the dbAvailable block and that it
exists at all, verified by restoring the original nesting -- the reviewer's exact case -- and by
deleting the check outright.
…sation Found by a cache-cleared isaac review of genie-agent against main. hasMcpDenial scanned the entire previousMessages array with .some(), matching any denied tool part anywhere in history. A denied part stays in the client's message history forever, so after a single denial every subsequent continuation matched it and returned res.end() -- approving a later tool silently dropped the continuation and the conversation stalled with no way forward. "Any denial anywhere" was never the question. "Was the interaction we are continuing FROM denied" is, so the check now looks at the most recent dynamic-tool part only. The scan itself predates 33fc179; that commit hoisted the block out of the dbAvailable guard, which was correct on its own terms and also widened where this fired -- it now applied in ephemeral mode too. Fixing the nesting without reading the predicate was an incomplete job. Verified against six cases, including the one that matters: a denial followed by an approval now proceeds where the old logic stopped. Invariant 49 gained two clauses -- the check must not be a blanket .some() over history, and something must identify the most recent tool part -- both proven by restoring the old shape.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional Agent panel to the SSO-SPN org view: a slide-out chat assistant backed by a curated Genie space (via MCP) and durable managed memory (Unity Catalog memory store).
The agent is space-scoped by default.
GENIE_MCP_MODE=spacewith aGENIE_SPACE_IDis what bootstrap deploys, because a guest user has no workspace entitlements and cannot query workspace-wide Genie at all. Workspace-wide remains available as an explicit opt-out (GENIE_MCP_MODE=one).Unlike the VSCode/notebook editors (Go proxy), the agent embeds through a Vercel-native reverse proxy at
/api/agent-proxythat mints a workspace bearer from the current user's mapped service principal — so guest users never hit the Databricks OAuth wall.The deployable Databricks App is assembled from the
vendor/app-templatessubmodule (agent-openai-agents-sdk+e2e-chatbot-app-next) plus a local overlay inagent/.Architecture
sequenceDiagram participant User participant Browser as Browser (Agent panel iframe) participant NextJS as Next.js (/api/agent-proxy) participant DB as Database participant OIDC as Databricks OIDC participant App as Agent App (Databricks) participant Genie as Genie space MCP participant Memory as UC memory store User->>Browser: Open Agent panel Browser->>NextJS: GET /api/agent-proxy NextJS->>DB: Resolve session, org, SPN mapping NextJS->>OIDC: client_credentials token NextJS->>App: Proxy with Bearer + HTML rewrite Browser->>NextJS: POST /api/agent-proxy/api/chat (SSE) NextJS->>App: Forward chat stream App->>Genie: ask_genie App->>Memory: read/write long-term memory App-->>Browser: Streamed answerSee also:
src/app/docs/solutions/agent/page.tsxandpublic/solutions/agent/architecture.mermaid.What's new
Frontend (Vercel)
src/components/agent/agent-panel.tsx— slide-out panel (feature-flagged)src/app/api/agent-proxy/[[...path]]/route.ts— SPN token minting, SSE proxy, HTML rewrite (<base>, light theme)src/stores/agent-panel-store.ts— panel open/minimize statesrc/app/sso-spn/[orgId]/layout.tsxAgent app (Databricks)
agent/overlay:agent_server/(Genie tools, managed memory), chat UI patches,databricks.ymlscripts/assemble_agent.sh— merges submodule + overlay →agent-build/agent/scripts/deploy_agent.sh,vendor_wheels.sh,setup_memory_store.py,start_app.pyAuth / guest fixes
src/lib/auth-dynamic.ts— Okta plugin only initializes when env vars are set (fixes Vercel build prerender crash)Docs / bootstrap
BOOTSTRAP.md+scripts/bootstrap.sh— end-to-end fresh-workspace runbook (Phases 0–9)Enable the panel
Frontend (
.env.local/ Vercel):Runtime prerequisites (or
/api/agent-proxyreturns 400/401):workspaceUrlsetuserSpns) for that orgCAN_USEon the agent app (documented in BOOTSTRAP Phase 6b)Agent app (bundle in
agent/databricks.yml, not frontend env):GENIE_MCP_MODE=one— workspace-wide Genie (opt-out; guests cannot use it)DATABRICKS_MEMORY_STORE— UC memory store FQN (must be created + granted post-deploy)Deploy path (happy path)
Or use
bash scripts/deploy_agent.sh <profile>(deploy → run → memory store).Full fresh-workspace walkthrough:
BOOTSTRAP.md.Known limitations
uv sync,npm install, and a Next.js production build before serving. Expect transient 503s; judge health onapp_status=RUNNING, not deployment status alone.setup_memory_store.pymust run after deploy or memory silently no-ops.USE CATALOG/USE SCHEMA/SELECT+ warehouseCAN_USEon schemas with tables.NEXT_PUBLIC_BETTER_AUTH_URL(causes CORS on hash-suffixed preview URLs). OmitSPN_AUTH_OKTA_*unless using Okta.Reviewer guide
Suggested read order:
src/app/docs/solutions/agent/page.tsxsrc/app/api/agent-proxy/[[...path]]/route.ts— token + proxyagent/agent_server/genie_tools.py— Genie MCP tools (path resolved bygenie_mcp.py)agent/agent_server/utils_memory.py— UC memory store REST APIscripts/assemble_agent.sh+agent/databricks.yml— packagingIntentional design choice: editors use the Go proxy; the agent uses the Vercel-native proxy (no Cloud Run / Go proxy required for the panel).
Test plan
Verified on a fresh workspace via
BOOTSTRAP.mdPhases 0–9:assemble_agent.sh+ submodule initapp_status=RUNNING(vendor wheels + bundle deploy)setup_memory_store.pycreates UC memory store and grants app SPNEXT_PUBLIC_BETTER_AUTH_URL)Since this PR was opened (114 commits)
The description above covers the original agent panel. Most of what is now on this branch
is a runbook plus the harness that found its defects, so this section records what
changed rather than rewriting the history above.
The runbook
BOOTSTRAP.mdis a harness-agnostic runbook an agent works through top to bottom, withscripts/bootstrap.shas its executable twin andscripts/check-runbook-invariants.shholding 55 invariants that keep the two in step. Every invariant was verified by
reverting its fix and confirming the check fails — several were rewritten after passing
against code that was deliberately broken.
What the harness found
An E2E harness provisions a clean VM and a fresh Databricks trial, hands a headless agent
the runbook, and scores the result against a 60-probe matrix. Twelve trial runs surfaced
21 defects, and the most valuable category was not crashes but output that pointed at
the wrong cause:
ok: no enabled IP allowlistMissing required field: principal$SP_CLIENT_ID, reported as a request-schema bugNo API found for 'PATCH /permissions/warehouses/'$WAREHOUSE_ID, reported as a missing API routedoes not exist or is deletedbundle deployment bindsuggestionCAN_RUNon the Genie spacePhase 6/8 also read variables that exist only in the first shell, so a second pass in a
fresh terminal produced errors that each blamed something other than the empty variable.
firefly_restore_phase6_contextand a cwd-independentstate.envfix that class.Phase ordering: the app is born in its final mode
The app used to deploy first and be corrected afterwards — Phase 4 shipped it workspace-wide,
and Phase 6c redeployed it onto the space once one existed. That left a window where the
deployed app pointed somewhere guests cannot query, and it made the redeploy race Phase 4's own
deployment (
400 Cannot update app ... pending deployment).Phase 3f now resolves the warehouse, seeds the data, and creates the space before Phase 4,
which deploys with
genie_mcp_modeandgenie_space_idalready set. Phase 6c is grants-onlyand redeploys nothing.
DEPLOY-AS-DOCUMENTEDasserts Phase 4 succeeds on the first attempt."Cannot do this yet" is no longer reported as "you said no"
Three messages told operators their Phase 0 answers had been discarded when they had not been:
no
--grant-guest. A missing flag defaulted tono, so an operator who answeredyeswastold the guest SP would get no access — while Phase 6c went on to grant it. Deferral is
now its own state (
--defer-grants) and names where the grants land.--seed noand the run reported seeding "declined at Phase 0" ona run that had just seeded 16 tables.
--seed skipmeans not this call's concern.not reached. The caller now passes
--phase.firefly_restore_phase6_contexthad the same shape of bug:state.envandinputs.envaredifferent files, it read only the first, and announced "assuming space" about a mode Phase 3f
had already written down.
Invariants 44 and 45 cover these, and each clause has been verified by sabotage. Two clauses
were vacuous when first written — a
grepover the whole file was satisfied by a dry-run bannerthat merely mentions the flag — and were rescoped until breaking the real call actually failed
them.
Genie One → Genie Agent
The product name is now Genie Agent throughout (49 sites). Where "Genie One" was carrying
the meaning workspace-wide, the text says workspace-wide instead — both modes are the
Genie Agent, so renaming alone would have made
oneandspaceread identically. TheGENIE_ONE_URLattribution link is gone: its audience is guest users with no workspaceaccess, so it led somewhere they could not open.
Surviving this merge
BOOTSTRAP.mdandscripts/exist only on this branch, so every branch reference was atime bomb. Both clone paths now resolve the branch and fall back to the default once it
carries the runbook, Phase 2 asserts the runbook actually arrived, and the README badge
asks for
FIREFLY_BRANCH(defaultmain) rather than pinning anything.Reviewer notes
(
{"enabled": false}on/actions/permissions), so the three workflows here have neverrun.
make checkruns the invariants and typecheck locally; enabling Actions needs anadmin and is the real fix.
inconclusive, 0 runbook gaps, with the agent itself reporting
RUNBOOK GAPS: None.N/A, and
ACL-ATTRIBUTIONneeds an enterprise-tier workspace, which a trial cannot be.